using System;
using System.IO;
using System.Runtime.InteropServices;
using System.Diagnostics;

namespace DupKiller.Core
{
	public class ReparsePoint
	{

		string normalisedTarget;
		string actualTarget;
		TagType tag;

		/// <summary>
		/// Takes a full path to a reparse point and finds the target.
		/// </summary>
		/// <param name="path">Full path of the reparse point</param>
		public ReparsePoint(string path)
		{
			Debug.Assert(!string.IsNullOrEmpty(path) && path.Length > 2 && path[1] == ':' && path[2] == '\\');
			normalisedTarget = "";
			tag = TagType.None;
			bool success;
			int lastError;
			// Apparently we need to have backup privileges
			IntPtr token;
			TOKEN_PRIVILEGES tokenPrivileges = new TOKEN_PRIVILEGES();
			tokenPrivileges.Privileges = new LUID_AND_ATTRIBUTES[1];
			success = OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES, out token);
			lastError = Marshal.GetLastWin32Error();
			if (success)
			{
				success = LookupPrivilegeValue(null, SE_BACKUP_NAME, out tokenPrivileges.Privileges[0].Luid); // null for local system
				lastError = Marshal.GetLastWin32Error();
				if (success)
				{
					tokenPrivileges.PrivilegeCount = 1;
					tokenPrivileges.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
					success = AdjustTokenPrivileges(token, false, ref tokenPrivileges, Marshal.SizeOf(tokenPrivileges), IntPtr.Zero, IntPtr.Zero);
					lastError = Marshal.GetLastWin32Error();
				}
				CloseHandle(token);
			}

			if (success)
			{
				// Open the file and get its handle
				IntPtr handle = CreateFile(path, FileAccess.Read, FileShare.None, 0, FileMode.Open, FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS, IntPtr.Zero);
				lastError = Marshal.GetLastWin32Error();
				if (handle.ToInt32() >= 0)
				{
					REPARSE_DATA_BUFFER buffer = new REPARSE_DATA_BUFFER();
					// Make up the control code - see CTL_CODE on ntddk.h
					uint controlCode = (FILE_DEVICE_FILE_SYSTEM << 16) | (FILE_ANY_ACCESS << 14) | (FSCTL_GET_REPARSE_POINT << 2) | METHOD_BUFFERED;
					uint bytesReturned;
					success = DeviceIoControl(handle, controlCode, IntPtr.Zero, 0, out buffer, MAXIMUM_REPARSE_DATA_BUFFER_SIZE, out bytesReturned, IntPtr.Zero);
					lastError = Marshal.GetLastWin32Error();
					if (success)
					{
						string subsString = "";
						string printString = "";
						// Note that according to http://wesnerm.blogs.com/net_undocumented/2006/10/symbolic_links_.html
						// Symbolic links store relative paths, while junctions use absolute paths
						// however, they can in fact be either, and may or may not have a leading \.
						Debug.Assert(buffer.ReparseTag == IO_REPARSE_TAG_SYMLINK || buffer.ReparseTag == IO_REPARSE_TAG_MOUNT_POINT,
							"Unrecognised reparse tag"); // We only recognise these two
						if (buffer.ReparseTag == IO_REPARSE_TAG_SYMLINK)
						{
							// for some reason symlinks seem to have an extra two characters on the front
							subsString = new string(buffer.ReparseTarget, (buffer.SubsNameOffset/2 + 2), buffer.SubsNameLength/2);
							printString = new string(buffer.ReparseTarget, (buffer.PrintNameOffset/2 + 2), buffer.PrintNameLength/2);
							tag = TagType.SymbolicLink;
						}
						else if (buffer.ReparseTag == IO_REPARSE_TAG_MOUNT_POINT)
						{
							// This could be a junction or a mounted drive - a mounted drive starts with "\\??\\Volume"
							subsString = new string(buffer.ReparseTarget, buffer.SubsNameOffset/2, buffer.SubsNameLength/2);
							printString = new string(buffer.ReparseTarget, buffer.PrintNameOffset/2, buffer.PrintNameLength/2);
							tag = subsString.StartsWith(@"\??\Volume") ? TagType.MountPoint : TagType.JunctionPoint;
						}
						Debug.Assert(!(string.IsNullOrEmpty(subsString) && string.IsNullOrEmpty(printString)), "Failed to retrieve parse point");
						// the printstring should give us what we want
						if (!string.IsNullOrEmpty(printString))
						{
							normalisedTarget = printString;
						}
						else
						{
							// if not we can use the substring with a bit of tweaking
							normalisedTarget = subsString;
							Debug.Assert(normalisedTarget.Length > 2, "Target string too short");
							Debug.Assert(
								(normalisedTarget.StartsWith(@"\??\") && (normalisedTarget[5] == ':' || normalisedTarget.StartsWith(@"\??\Volume")) ||
									(!normalisedTarget.StartsWith(@"\??\") && normalisedTarget[1] != ':')),
								"Malformed subsString");
							// Junction points must be absolute
							Debug.Assert(
								buffer.ReparseTag == IO_REPARSE_TAG_SYMLINK ||
									normalisedTarget.StartsWith(@"\??\Volume") ||
										normalisedTarget[1] == ':',
								"Relative junction point");
							if (normalisedTarget.StartsWith(@"\??\"))
							{
								normalisedTarget = normalisedTarget.Substring(4);
							}
						}
						actualTarget = normalisedTarget;
						// Symlinks can be relative.
						if (buffer.ReparseTag == IO_REPARSE_TAG_SYMLINK && (normalisedTarget.Length < 2 || normalisedTarget[1] != ':'))
						{
							// it's relative, we need to tack it onto the path
							if (normalisedTarget[0] == '\\')
							{
								normalisedTarget = normalisedTarget.Substring(1);
							}
							if (path.EndsWith(@"\"))
							{
								path = path.Substring(0, path.Length - 1);
							}
							// Need to take the symlink name off the path
							normalisedTarget = path.Substring(0, path.LastIndexOf('\\')) + @"\" + normalisedTarget;
							// Note that if the symlink target path contains any ..s these are not normalised but returned as is.
						}
						// Remove any final slash for consistency
						if (normalisedTarget.EndsWith("\\"))
						{
							normalisedTarget = normalisedTarget.Substring(0, normalisedTarget.Length - 1);
						}
					}
					CloseHandle(handle);
				}
				else
				{
					success = false;
				}
			}
		}

		/// <summary>
		/// This returns the normalised target, ie. if the actual target is relative it has been made absolute
		/// Note that it is not fully normalised in that .s and ..s may still be included.
		/// </summary>
		/// <returns>The normalised path</returns>
		public override string ToString()
		{
			return normalisedTarget;
		}

		/// <summary>
		/// Gets the actual target string, before normalising
		/// </summary>
		public string Target
		{
			get { return actualTarget; }
		}

		/// <summary>
		/// Gets the tag
		/// </summary>
		public TagType Tag
		{
			get { return tag; }
		}
	}
}